﻿//
// (C) Copyright 2009 Irantha Suwandarathna (iranthas@hotmail.com)
// All rights reserved.
//

/* Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using EffiProz.Core.Lib;
using EffiProz.Core.DataTypes;
using EffiProz.Core.Indexes;
using EffiProz.Core.Errors;
using EffiProz.Core.Persist;
using EffiProz.Core.Results;

namespace EffiProz.Core
{
    public abstract class StatementDMQL : Statement
    {
        public static String PCOL_PREFIX = "@p";
        public static String RETURN_COLUMN_NAME = "@p0";

        /** target table for INSERT_XXX, UPDATE and DELETE and MERGE */
        public Table targetTable;
        public Table baseTable;

        /** column map of query expression */
        public int[] baseColumnMap;
        public RangeVariable[] targetRangeVariables;

        /** source table for MERGE */
        public Table sourceTable;

        /** condition expression for UPDATE, MERGE and DELETE */
        public Expression condition;

        /** for TRUNCATE variation of DELETE */
        public bool restartIdentity;

        /** column map for INSERT operation direct or via MERGE */
        public int[] insertColumnMap;

        /** column map for UPDATE operation direct or via MERGE */
        public int[] updateColumnMap;
        public int[] baseUpdateColumnMap;

        /** Column value Expressions for UPDATE and MERGE. */
        public Expression[] updateExpressions;

        /** Column value Expressions for MERGE */
        public Expression[][] multiColumnValues;

        /** INSERT_VALUES */
        public Expression insertExpression;

        /**
         * Flags indicating which columns' values will/will not be
         * explicitly set.
         */
        public bool[] insertCheckColumns;
        public bool[] updateCheckColumns;

        /**
         * Select to be evaluated when this is an INSERT_SELECT or
         * SELECT statement
         */
        public QueryExpression queryExpression;

        /**
         * Parse-order array of Expression objects, all of type PARAMETER ,
         * involved in some way in any INSERT_XXX, UPDATE, DELETE, SELECT or
         * CALL CompiledStatement
         */
        public ExpressionColumn[] parameters;

        /**
         * int[] contains column indexes for generated values
         */
        public int[] generatedIndexes;

        /**
         * ResultMetaData for generated values
         */
        public ResultMetaData _generatedResultMetaData;

        /**
         * ResultMetaData for parameters
         */
        public ResultMetaData parameterMetaData;

        /**
         * Subqueries inverse usage depth order
         */
        public SubQuery[] subqueries = SubQuery.emptySubqueryArray;

        /**
         * The type of this CompiledStatement. <p>
         *
         * One of: <p>
         *
         * <ol>
         *  <li>UNKNOWN
         *  <li>INSERT_VALUES
         *  <li>INSERT_SELECT
         *  <li>UPDATE
         *  <li>DELETE
         *  <li>SELECT
         *  <li>CALL
         *  <li>MERGE
         *  <li>DDL
         * </ol>
         */

        /**
         * Total number of RangeIterator objects used
         */
        public int rangeIteratorCount;

        /**
         * Database objects used
         */
        public NumberSequence[] sequences;
        public Routine[] routines;
        public RangeVariable[] rangeVariables;

        public StatementDMQL(int type, int group, QNameManager.QName schemaName)
            : base(type, group)
        {

            this.schemaName = schemaName;
            this._isTransactionStatement = true;
        }

        public void setBaseIndexColumnMap()
        {

            if (targetTable != baseTable)
            {
                baseColumnMap = targetTable.getBaseTableColumnMap();
            }
        }

        public override Result execute(Session session)
        {

            Result result = getAccessRightsResult(session);

            if (result != null)
            {
                return result;
            }

            if (this.isExplain)
            {
                return Result.newSingleColumnStringResult("OPERATION",
                        describe(session));
            }

            if (session.sessionContext.dynamicArguments.Length
                    != parameters.Length)
            {

                //            return Result.newErrorResult(Error.error(ErrorCode.X_42575));
            }

            try
            {
                materializeSubQueries(session);

                result = getResult(session);
            }
            catch (Exception t)
            {
                session.sessionContext.clearStructures(this);

                result = Result.newErrorResult(t, null);

                result.getException().setStatementType(group, type);
            }

            session.sessionContext.clearStructures(this);

            return result;
        }

        public abstract Result getResult(Session session);

        public abstract void collectTableNamesForRead(OrderedHashSet set);

        /**
         * @todo - fredt - this does not work with different prepare calls
         * with the same SQL statement, but different generated column requests
         * To fix, add comment encapsulating the generated column list to SQL
         * to differentiate between the two invocations
         */
        public override void setGeneratedColumnInfo(int generate, ResultMetaData meta)
        {

            // can support INSERT_SELECT also
            if (type != StatementTypes.INSERT)
            {
                return;
            }

            int colIndex = baseTable.getIdentityColumnIndex();

            if (colIndex == -1)
            {
                return;
            }

            switch (generate)
            {

                case ResultConstants.RETURN_NO_GENERATED_KEYS:
                    return;

                case ResultConstants.RETURN_GENERATED_KEYS_COL_INDEXES:
                    int[] columnIndexes = meta.getGeneratedColumnIndexes();

                    if (columnIndexes.Length != 1)
                    {
                        return;
                    }

                    if (columnIndexes[0] != colIndex)
                    {
                        return;
                    }
                    goto case ResultConstants.RETURN_GENERATED_KEYS;
                // fall through
                case ResultConstants.RETURN_GENERATED_KEYS:
                    generatedIndexes = new int[] { colIndex };
                    break;

                case ResultConstants.RETURN_GENERATED_KEYS_COL_NAMES:
                    String[] columnNames = meta.getGeneratedColumnNames();

                    if (columnNames.Length != 1)
                    {
                        return;
                    }

                    if (baseTable.findColumn(columnNames[0]) != colIndex)
                    {
                        return;
                    }

                    generatedIndexes = new int[] { colIndex };
                    break;
            }

            _generatedResultMetaData =
                ResultMetaData.newResultMetaData(generatedIndexes.Length);

            for (int i = 0; i < generatedIndexes.Length; i++)
            {
                ColumnSchema column = baseTable.getColumn(generatedIndexes[i]);

                _generatedResultMetaData.columns[i] = column;
            }

            _generatedResultMetaData.prepareData();
        }

        public Object[] getGeneratedColumns(Object[] data)
        {

            if (generatedIndexes == null)
            {
                return null;
            }

            Object[] values = new Object[generatedIndexes.Length];

            for (int i = 0; i < generatedIndexes.Length; i++)
            {
                values[i] = data[generatedIndexes[i]];
            }

            return values;
        }

        public override bool hasGeneratedColumns()
        {
            return generatedIndexes != null;
        }

        public override ResultMetaData generatedResultMetaData()
        {
            return _generatedResultMetaData;
        }

        public bool[] getInsertOrUpdateColumnCheckList()
        {

            switch (type)
            {

                case StatementTypes.INSERT:
                    return insertCheckColumns;

                case StatementTypes.UPDATE_WHERE:
                    return updateCheckColumns;

                case StatementTypes.MERGE:
                    bool[] check =
                        (bool[])ArrayUtil.duplicateArray(insertCheckColumns);

                    ArrayUtil.orBooleanArray(updateCheckColumns, check);

                    return check;
            }

            return null;
        }

        private void setParameters()
        {

            for (int i = 0; i < parameters.Length; i++)
            {
                parameters[i].parameterIndex = i;
            }
        }

       public  void materializeSubQueries(Session session)
        {

            if (subqueries.Length == 0)
            {
                return;
            }

            HashSet subqueryPopFlags = new HashSet();

            for (int i = 0; i < subqueries.Length; i++)
            {
                SubQuery sq = subqueries[i];

                // VIEW working tables may be reused in a single query but they are filled only once
                if (!subqueryPopFlags.add(sq))
                {
                    continue;
                }

                if (!sq.isCorrelated())
                {
                    sq.materialise(session);
                }
            }
        }

        public override void clearVariables()
        {

            _isValid = false;
            targetTable = null;
            baseTable = null;
            condition = null;
            insertColumnMap = null;
            updateColumnMap = null;
            updateExpressions = null;
            insertExpression = null;
            insertCheckColumns = null;

            //        expression         = null;
            parameters = null;
            subqueries = null;
        }

        public void setDatabseObjects(ParserDML.CompileContext compileContext)
        {

            parameters = compileContext.getParameters();

            setParameters();
            setParameterMetaData();

            subqueries = compileContext.getSubqueries();
            rangeIteratorCount = compileContext.getRangeVarCount();
            rangeVariables = compileContext.getRangeVariables();
            sequences = compileContext.getSequences();
            routines = compileContext.getRoutines();

            OrderedHashSet set = new OrderedHashSet();

            collectTableNamesForRead(set);

            for (int i = 0; i < routines.Length; i++)
            {
                set.addAll(routines[i].getTableNamesForRead());
            }

            if (set.size() > 0)
            {
                readTableNames = new QNameManager.QName[set.size()];

                set.toArray(readTableNames);
                set.clear();
            }

            // other fk references this :  if constraint trigger action  : other write lock
            if (baseTable != null)
            {
                if (baseTable.isTemp())
                {
                    return;
                }

                set.add(baseTable.getName());

                for (int i = 0; i < baseTable.fkPath.Length; i++)
                {
                    set.add(baseTable.fkPath[i].getMain().getName());
                }

                getTriggerTableNames(set, true);
            }

            for (int i = 0; i < routines.Length; i++)
            {
                set.addAll(routines[i].getTableNamesForWrite());
            }

            if (set.size() > 0)
            {
                writeTableNames = new QNameManager.QName[set.size()];

                set.toArray(writeTableNames);
            }

            references = compileContext.getSchemaObjectNames();
        }

        public virtual void getTriggerTableNames(OrderedHashSet set, bool write) { }

        /**
         * Determines if the authorizations are adequate
         * to execute the compiled object. Completion requires the list of
         * all database objects in a compiled statement.
         */
        public virtual void checkAccessRights(Session session)
        {

            if (targetTable != null && !targetTable.isTemp())
            {
                if (targetTable.getOwner().isSystem())
                {
                    if (!session.getUser().isSystem())
                    {
                        throw Error.error(ErrorCode.X_42501,
                                           targetTable.getName().name);
                    }
                }

                targetTable.checkDataReadOnly();
                session.checkReadWrite();
            }

            if (session.isAdmin())
            {
                return;
            }

            for (int i = 0; i < sequences.Length; i++)
            {
                session.getGrantee().checkAccess(sequences[i]);
            }

            for (int i = 0; i < routines.Length; i++)
            {
                if (routines[i].isLibraryRoutine())
                {
                    continue;
                }

                session.getGrantee().checkAccess(routines[i]);
            }

            for (int i = 0; i < rangeVariables.Length; i++)
            {
                RangeVariable range = rangeVariables[i];

                if (range.rangeTable.getSchemaName()
                        == SqlInvariants.SYSTEM_SCHEMA_QNAME)
                {
                    continue;
                }

                session.getGrantee().checkSelect(range.rangeTable,
                                                 range.usedColumns);
            }

            switch (type)
            {

                case StatementTypes.CALL:
                    {
                        break;
                    }
                case StatementTypes.INSERT:
                    {
                        session.getGrantee().checkInsert(targetTable,
                                                         insertCheckColumns);

                        break;
                    }
                case StatementTypes.SELECT_CURSOR:
                    break;

                case StatementTypes.DELETE_WHERE:
                    {
                        session.getGrantee().checkDelete(targetTable);

                        break;
                    }
                case StatementTypes.UPDATE_WHERE:
                    {
                        session.getGrantee().checkUpdate(targetTable,
                                                         updateCheckColumns);

                        break;
                    }
                case StatementTypes.MERGE:
                    {
                        session.getGrantee().checkInsert(targetTable,
                                                         insertCheckColumns);
                        session.getGrantee().checkUpdate(targetTable,
                                                         updateCheckColumns);

                        break;
                    }
            }
        }

        Result getAccessRightsResult(Session session)
        {

            try
            {
                checkAccessRights(session);
            }
            catch (CoreException e)
            {
                return Result.newErrorResult(e);
            }

            return null;
        }

        /**
         * Returns the metadata, which is empty if the CompiledStatement does not
         * generate a Result.
         */
        public override ResultMetaData getResultMetaData()
        {

            switch (type)
            {

                case StatementTypes.DELETE_WHERE:
                case StatementTypes.INSERT:
                case StatementTypes.UPDATE_WHERE:
                    return ResultMetaData.emptyResultMetaData;

                default:
                    throw Error.runtimeError(ErrorCode.U_S0500, "StatementDMQL");
            }
        }

        /** @todo 1.9.0 - build the metadata only once and reuse */

        /**
         * Returns the metadata for the placeholder parameters.
         */
        public override ResultMetaData getParametersMetaData()
        {
            return parameterMetaData;
        }

        public void setParameterMetaData()
        {

            int offset;
            int idx;
            //bool hasReturnValue;

            offset = 0;

            if (parameters.Length == 0)
            {
                parameterMetaData = ResultMetaData.emptyParamMetaData;

                return;
            }

            // NO:  Not yet
            //        hasReturnValue = (type == CALL && !expression.isProcedureCall());
            //
            //        if (hasReturnValue) {
            //            outlen++;
            //            offset = 1;
            //        }
            parameterMetaData =
                ResultMetaData.newParameterMetaData(parameters.Length);

            // NO: Not yet
            //        if (hasReturnValue) {
            //            e = expression;
            //            out.sName[0]       = DIProcedureInfo.RETURN_COLUMN_NAME;
            //            out.sClassName[0]  = e.getValueClassName();
            //            out.colType[0]     = e.getDataType();
            //            out.colSize[0]     = e.getColumnSize();
            //            out.colScale[0]    = e.getColumnScale();
            //            out.nullability[0] = e.nullability;
            //            out.isIdentity[0]  = false;
            //            out.paramMode[0]   = expression.PARAM_OUT;
            //        }
            for (int i = 0; i < parameters.Length; i++)
            {
                idx = i + offset;

                // always i + 1.  We currently use the convention of @p0 to name the
                // return value OUT parameter
                parameterMetaData.columnLabels[idx] = parameters[i].opType == OpTypes.NAMED_PARAM ?
                    parameters[i].columnName : StatementDMQL.PCOL_PREFIX  + (i + 1);
                parameterMetaData.columnTypes[idx] = parameters[i].dataType;

                byte parameterMode = SchemaObjectParameterModes.PARAM_IN;

                if (parameters[i].column != null
                        && parameters[i].column.getParameterMode()
                           != SchemaObjectParameterModes.PARAM_UNKNOWN)
                {
                    parameterMode = parameters[i].column.getParameterMode();
                }

                parameterMetaData.paramModes[idx] = parameterMode;
                parameterMetaData.paramNullable[idx] =
                    parameters[i].column == null
                    ? SchemaObjectNullability.NULLABLE
                    : parameters[i].column.getNullability();
            }
        }

        /**
         * Retrieves a String representation of this object.
         */
        public override String describe(Session session)
        {

            try
            {
                return describeImpl(session);
            }
            catch (Exception e)
            {
                // e.printStackTrace();

                return e.ToString();
            }
        }

        /**
         * Provides the toString() implementation.
         */
        private String describeImpl(Session session)
        {

            StringBuilder sb;

            sb = new StringBuilder();

            switch (type)
            {

                case StatementTypes.SELECT_CURSOR:
                    {
                        sb.Append(queryExpression.describe(session));
                        appendParms(sb).Append('\n');
                        appendSubqueries(session, sb);

                        return sb.ToString();
                    }
                case StatementTypes.INSERT:
                    {
                        if (queryExpression == null)
                        {
                            sb.Append("INSERT VALUES");
                            sb.Append('[').Append('\n');
                            appendMultiColumns(sb, insertColumnMap).Append('\n');
                            appendTable(sb).Append('\n');
                            appendParms(sb).Append('\n');
                            appendSubqueries(session, sb).Append(']');

                            return sb.ToString();
                        }
                        else
                        {
                            sb.Append("INSERT SELECT");
                            sb.Append('[').Append('\n');
                            appendColumns(sb, insertColumnMap).Append('\n');
                            appendTable(sb).Append('\n');
                            sb.Append(queryExpression.describe(session)).Append('\n');
                            appendParms(sb).Append('\n');
                            appendSubqueries(session, sb).Append(']');

                            return sb.ToString();
                        }
                    }
                case StatementTypes.UPDATE_WHERE:
                    {
                        sb.Append("UPDATE");
                        sb.Append('[').Append('\n');
                        appendColumns(sb, updateColumnMap).Append('\n');
                        appendTable(sb).Append('\n');
                        appendCondition(session, sb);
                        sb.Append(targetRangeVariables[0].describe(session)).Append(
                            '\n');
                        sb.Append(targetRangeVariables[1].describe(session)).Append(
                            '\n');
                        appendParms(sb).Append('\n');
                        appendSubqueries(session, sb).Append(']');

                        return sb.ToString();
                    }
                case StatementTypes.DELETE_WHERE:
                    {
                        sb.Append("DELETE");
                        sb.Append('[').Append('\n');
                        appendTable(sb).Append('\n');
                        appendCondition(session, sb);
                        sb.Append(targetRangeVariables[0].describe(session)).Append(
                            '\n');
                        sb.Append(targetRangeVariables[1].describe(session)).Append(
                            '\n');
                        appendParms(sb).Append('\n');
                        appendSubqueries(session, sb).Append(']');

                        return sb.ToString();
                    }
                case StatementTypes.CALL:
                    {
                        sb.Append("CALL");
                        sb.Append('[').Append(']');

                        return sb.ToString();
                    }
                case StatementTypes.MERGE:
                    {
                        sb.Append("MERGE");
                        sb.Append('[').Append('\n');
                        appendMultiColumns(sb, insertColumnMap).Append('\n');
                        appendColumns(sb, updateColumnMap).Append('\n');
                        appendTable(sb).Append('\n');
                        appendCondition(session, sb);
                        sb.Append(targetRangeVariables[0].describe(session)).Append(
                            '\n');
                        sb.Append(targetRangeVariables[1].describe(session)).Append(
                            '\n');
                        sb.Append(targetRangeVariables[2].describe(session)).Append(
                            '\n');
                        appendParms(sb).Append('\n');
                        appendSubqueries(session, sb).Append(']');

                        return sb.ToString();
                    }
                default:
                    {
                        return "UNKNOWN";
                    }
            }
        }

        private StringBuilder appendSubqueries(Session session, StringBuilder sb)
        {

            sb.Append("SUBQUERIES[");

            for (int i = 0; i < subqueries.Length; i++)
            {
                sb.Append("\n[level=").Append(subqueries[i].level).Append('\n');

                if (subqueries[i].queryExpression != null)
                {
                    sb.Append(subqueries[i].queryExpression.describe(session));
                }

                sb.Append("]");
            }

            sb.Append(']');

            return sb;
        }

        private StringBuilder appendTable(StringBuilder sb)
        {

            sb.Append("TABLE[").Append(targetTable.getName().name).Append(']');

            return sb;
        }

        private StringBuilder appendSourceTable(StringBuilder sb)
        {

            sb.Append("SOURCE TABLE[").Append(sourceTable.getName().name).Append(
                ']');

            return sb;
        }

        private StringBuilder appendColumns(StringBuilder sb, int[] columnMap)
        {

            if (columnMap == null || updateExpressions == null)
            {
                return sb;
            }

            sb.Append("COLUMNS=[");

            for (int i = 0; i < columnMap.Length; i++)
            {
                sb.Append('\n').Append(columnMap[i]).Append(':').Append(
                    ' ').Append(
                    targetTable.getColumn(columnMap[i]).getNameString()).Append(
                    '[').Append(updateExpressions[i]).Append(']');
            }

            sb.Append(']');

            return sb;
        }

        private StringBuilder appendMultiColumns(StringBuilder sb, int[] columnMap)
        {

            if (columnMap == null || multiColumnValues == null)
            {
                return sb;
            }

            sb.Append("COLUMNS=[");

            for (int j = 0; j < multiColumnValues.Length; j++)
            {
                for (int i = 0; i < columnMap.Length; i++)
                {
                    sb.Append('\n').Append(columnMap[i]).Append(':').Append(
                        ' ').Append(
                        targetTable.getColumn(columnMap[i]).getName().name).Append(
                        '[').Append(multiColumnValues[j][i]).Append(']');
                }
            }

            sb.Append(']');

            return sb;
        }

        private StringBuilder appendParms(StringBuilder sb)
        {

            sb.Append("PARAMETERS=[");

            for (int i = 0; i < parameters.Length; i++)
            {
                sb.Append('\n').Append('@').Append(i).Append('[').Append(
                    parameters[i]).Append(']');
            }

            sb.Append(']');

            return sb;
        }

        private StringBuilder appendCondition(Session session, StringBuilder sb)
        {

            return condition == null ? sb.Append("CONDITION[]\n")
                                     : sb.Append("CONDITION[").Append(
                                         condition.describe(session)).Append(
                                         "]\n");
        }

        public override void resolve() { }

        public override RangeVariable[] getRangeVariables()
        {
            return rangeVariables;
        }
    }
}
